<?php
/**
* 一个基于 PHP 的数学运算验证码生成类
* LICENSE:  Apache Lincense 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
* Author:    KaMingChung
*/

class MathCaptcha {
  
  /**
   * 字体的位置，默认的是思源黑体简化版
   * @var string
   */
  public $fontPath           = "./font/SourceHanSansCN-Normal.min.ttf";

  /**
   * 设置输出的配置
   */
  public $defaultOutputConf1 = array(
    "height" => 40, 
    "width"  => 100,
    "noise"  => 200,
    "line"   => 1,
    array(
    "fontSizeRangeMin" => 12, 
    "fontSizeRangeMax" => 20
    )
  );
  public $defaultOutputConf2 = array(
    "height" => 50,
    "width"  => 125,
    "noise"  => 300,
    "line"   => 1,
    array(
      "fontSizeRangeMin" => 15, 
      "fontSizeRangeMax" => 25
    )
  );
  public $defaultOutputConf3 = array(
    "height" => 60 , 
    "width"  => 150, 
    "noise"  => 500,
    "line"   => 1,
    array(
      "fontSizeRangeMin" => 18, 
      "fontSizeRangeMax" => 30
    )
  );

  /**
   * 默认的输出配置设置
   * @var [type]
   */
  public $defaultOutputConfig;

  /**
   * 设置输出的格式
   * @var string
   */
  public $outputImageType  = "png";

  /**
   * 是否需要干扰线
   * @var boolean
   */
  public $outputImageLine  = true;

  /**
   * 是否需要噪点
   * @var boolean
   */
  public $outputImageNoise = true;

  /**
   * 字符显示模式
   */
  const MIX    = 1; //中文字符与阿拉伯数字混合
  const DIGIT  = 2; //仅阿拉伯数字
  const CHNCHR = 3; //仅中文字符

  /** 
   * 计算方式
   * @var array
   */
  public $calculateModel = array("add", "subtract", "multiply", "divide");

  /**
   * 数字到文字的映射
   * @var array
   */
  public $numberMapping   = array(
    1 => array(1, "一", "壹"),
    2 => array(2, "二", "貳"),
    3 => array(3, "三", "叄"),
    4 => array(4, "四", "肆"),
    5 => array(5, "五", "伍"),
    6 => array(6, "六", "陆"),
    7 => array(7, "七", "柒"),
    8 => array(8, "八", "捌"),
    9 => array(9, "九", "玖"),
    0 => array(0, "零", "零"),
  );

  /**
   * 操作符的映射
   * @var array
   */
  public $operationMapping = array(
    "add"      => array("＋", "加"),
    "subtract" => array("－", "减"),
    "multiply" => array("×", "乘"),
    "divide"   => array("÷", "除"),
  );

  /**
   * 表达式计算的结果
   * @var float
   */
  public $result;

  /**
   * 颜色范围
   * @var array
   */
  public $defaultColorRange  = array("min" => "50", "max" => "200");

  /**
   * 字体的倾斜角度
   * @var array
   */
  public $fontTiltAngleRange = array("min" => -25, "max" => 25);

  /**
   * 验证码图像
   * @var resource
   */
  private $captcha;

  /**
   * 生成一个算术表达式
   * @param  string $calModel 计算的方式
   * @return array            0-第一个数字 1-计算方式 2-第二个数字
   */
  private function generateExpression() {
    $calModel    = $this->calculateModel[mt_rand(0, count($this->calculateModel) - 1)];
    $firstNumber = $this->generateNumber($calModel);
    $secNumber   = $this->generateNumber($calModel);

    if ("add"            === $calModel) {
    $tempExp             = array($firstNumber, $calModel, $secNumber );
    $this->result        = ($firstNumber + $secNumber);
    } elseif ("subtract" === $calModel) {
    //如果后数大于前数，为了不出现算出负值的现象，反转它们
    $tempExp             = ($secNumber > $firstNumber) ?
      array($secNumber, $calModel, $firstNumber) :
      array($firstNumber, $calModel, $secNumber);
    $this->result        = ($tempExp[0] - $tempExp[2]);
    } elseif ("multiply" === $calModel) {
    $tempExp             = array($firstNumber, $calModel, $secNumber);
    $this->result        = ($firstNumber * $secNumber);
    } elseif ("divide"   === $calModel) {
    //两个都是0的话，重置除数
    while ($firstNumber  === 0 && $secNumber === 0) $secNumber = $this->generateNumber($calModel);
    //仅仅后者是0的话，反转它们
    $tempExp             = ($secNumber === 0) ?
      array($secNumber, $calModel, $firstNumber) :
      array($firstNumber, $calModel, $secNumber);
    $tempResult          = $tempExp[0] * $tempExp[2];
    //重新生成
    $tempExp             = array($tempResult, $calModel, $tempExp[2]);
    $this->result        = $tempExp[0];
    }
    return $tempExp;
  }
  /**
   * 根据传入的计算方式生成适当的随机数字
   * @param  string  $calModel             计算方式
   * @return float                         随机数
   */
  private function generateNumber($calModel) {
    switch ($calModel) {
       case 'add':
       case 'subtract':
        return (float)mt_rand(0, 9);
        break;

      case 'multiply':
      case 'divide':
        return (float)mt_rand(0, 9);
        break;
     }
  }

  /**
   * 根据给定的数字数组按设置生成文字数组
   * @param  array $expArray  输入的数组，一般来自generateExpression，但也可以由你手动设置
   * @param  float $ChrModel  输出的字符方式
   * @return array            数组中的第一个数组由于多字，以数组输出，其余以字符串输出
   */
  private function getCharacterByExpressionArray($expArray, $chrModel) {
    $firstNumberCharacter = str_split($expArray[0]);
    foreach ($firstNumberCharacter as $value) {
      if (self::MIX          === $chrModel) {
        $_temp[]               = $this->numberMapping[$value][mt_rand(0,2)];
      } elseif (self::DIGIT  === $chrModel) {
        $_temp[]               = $this->numberMapping[$value][0];
      } elseif (self::CHNCHR === $chrModel) {
        $_temp[]               = $this->numberMapping[$value][mt_rand(1,2)];
      }
    }
    $chrArray[] = $_temp;
    switch ($chrModel) {
      default:
      case self::MIX:
        $chrArray[] = $this->operationMapping[$expArray[1]][mt_rand(0,1)];
        $chrArray[] = $this->numberMapping[$expArray[2]][mt_rand(0,2)];
        break;
      case self::DIGIT:
        $chrArray[] = $this->operationMapping[$expArray[1]][0];
        $chrArray[] = $this->numberMapping[$expArray[2]][0];
        break;
      case self::CHNCHR:
        $chrArray[] = $this->operationMapping[$expArray[1]][1];
        $chrArray[] = $this->numberMapping[$expArray[2]][mt_rand(1,2)];
        break;
    }
    return $chrArray;
  }

  /**
   * 运行并输出验证码
   * @param  array $outputConf 输入一个数组，这个数组作为输出的配置使用，参考defaultOutputConf1
   * @param  array $character  由getCharacterByExpressionArray返回的文字数组
   */
  private function run($outputConf, $character) {
    header("Content-Type: image/" . $this->outputImageType);
    $fontSpacing   = (1 === count($character[0])) ?
    mt_rand(0, $outputConf["width"] / 4) :
    mt_rand(0, 20);
    $this->captcha = $captcha = imagecreatetruecolor($outputConf["width"], $outputConf["height"]) or die("无法创建gd图像。");
    imageantialias($captcha, true);
    $background    = imagecolorallocate($captcha, 255, 255, 255);
    imagefilledrectangle($captcha, 0, 0, $outputConf["width"], $outputConf["height"], $background);
    foreach ($character as $char) {
      $char           = is_array($char) ? implode($char) : $char; //看看是一个字还是两个字
      $angleTemp      = $this->getAngle();//得到一个角度
      $textSize       = $this->getTextSize($char, $angleTemp, $outputConf[0]);
      //得到随机字体大小，算出字体高度宽度
      $charStartPoint = ($textSize["textSize"]) + mt_rand(0, $outputConf["height"] - $textSize["textHeight"]);
      //字体在垂直线上的起始
      $colorRange     = $this->generateColor($this->defaultColorRange);
      $randomColor    = imagecolorallocate($captcha, $colorRange["red"], $colorRange["green"], $colorRange["blue"]);
      imagettftext($captcha, $textSize["textSize"], $this->getAngle(), $fontSpacing, $charStartPoint, $randomColor, $this->fontPath, $char);
      $fontSpacing    += $textSize["textWidth"];
    }
    $this->outputImageNoise && $this->addNoise($outputConf["noise"], $outputConf["height"], $outputConf["width"]);
    $this->outputImageLine  && $this->addLine($outputConf["line"], $outputConf["height"], $outputConf["width"]);
    //控制噪点和线段
    $this->destory();
  }

  /**
   * 清理资源
   */
  private function destory() {
    $outputType = "image" . $this->outputImageType;
    $outputType($this->captcha);
    imagedestroy($this->$captcha);
  }

  /**
   * 返回随机计算的角度
   * @return float 随机角度
   */
  private function getAngle() {
    return (float)mt_rand($this->fontTiltAngleRange["min"], $this->fontTiltAngleRange["max"]);
  }

  /**
   * 获得随机字符大小，并计算高宽
   * @param  string $text           要计算的文字
   * @param  float  $angle          文字旋转角度
   * @param  array  $fontSizeRange  传入的高宽
   * @return array                  返回文字大小，文字高宽
   */
  private function getTextSize($text, $angle, $fontSizeRange) {
    $fontSize               = mt_rand($fontSizeRange["fontSizeRangeMin"], $fontSizeRange["fontSizeRangeMax"]);
    $textBox                = imagettfbbox($fontSize, $angle, $this->fontPath, $text);
    $width                  = $textBox[2] - $textBox[0];
    $height                 = $textBox[1] - $textBox[7];
    return array(
      "textSize" => $fontSize, 
      "textHeight" => $height, 
      "textWidth" => $width
    );
  }

  /**
   * 根据颜色范围，生成一个字体颜色
   * @param  array $colorRange 颜色范围
   * @return array             随机出来的颜色
   */
  private function generateColor($colorRange) {
    $range["red"]   = mt_rand($colorRange["min"], $colorRange["max"]);
    $range["green"] = mt_rand($colorRange["min"], $colorRange["max"]);
    $range["blue"]  = mt_rand($colorRange["min"], $colorRange["max"]);
    return $range;
  }

  /**
   * 为验证码添加噪点
   * @param float $noiseQuantity 噪点数量
   * @param float $imageHeight   验证码的高度
   * @param float $imageWidth    验证码的宽度
   */
  private function addNoise($noiseQuantity, $imageHeight, $imageWidth) {
    $noiseColor   = imagecolorallocate($this->captcha, 255, 255, 255);
    for ($i       = 0; $i < $noiseQuantity; $i++) {
    $pixelX       = mt_rand(0, $imageWidth);
    $pixelY       = mt_rand(0, $imageHeight);
    imagesetpixel($this->captcha, $pixelX, $pixelY, $noiseColor);
    }
  }

  /**
   * 为验证码添加干扰线
   * @param float $noiseQuantity 干扰线数量
   * @param float $imageHeight   验证码的高度
   * @param float $imageWidth    验证码的宽度
   */
  private function addLine($lineQuantity, $outputHeight, $outputWidth) {
    for ($i     =0; $i < $lineQuantity; $i++) { 
    $startPoint = array(
    "x"         => mt_rand(0, $outputWidth / 4),
    "y"         => mt_rand(0, $outputHeight)
    );
    $endPoint   = array(
    "x"         => mt_rand( ($outputWidth - $outputWidth / 4), $outputWidth),
    "y"         => mt_rand(0, $outputHeight)
    );
    $lineColor  = $this->generateColor($this->defaultColorRange);
    imageline($this->captcha, $startPoint["x"], $startPoint["y"], $endPoint["x"], $endPoint["y"], imagecolorallocate($this->captcha, $lineColor["red"], $lineColor["green"], $lineColor["blue"]));
    }
  }

  /**
   * 全局构造方法
   * @param boolean $autoRun          如此值为true，会直接调用output方法输出
   * @param float  $charDisplayModel  设置字符输出的方式
   */
  public function __construct($autoRun = false, $charDisplayModel = self::CHNCHR) {
    $this->defaultOutputConfig = $this->defaultOutputConf1; //设置默认的输出配置
    $autoRun && $this->output($charDisplayModel);
  }

  /**
   * 输出函数
   * @param  float $charDisplayModel  字符输出的方式
   */
  public function output($charDisplayModel = self::CHNCHR) {
    $this->run($this->defaultOutputConfig, $this->getCharacterByExpressionArray($this->generateExpression(), $charDisplayModel));
  }
}